﻿using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Threading;

namespace Threading
{
    /// <summary>
    /// Class with purpose to download unlimited number of images in multithreaded manner from the web.
    /// </summary>
    public abstract class ThreadedWorkerManager<TWorkItemState>
    {
        protected delegate void AllWorkItemsFinishedHandler();
        public delegate void WorkItemFinishedHandler(object sender, WorkItemEventArgs args);
        public delegate void WorkItemStartedHandler(object sender, WorkItemEventArgs args);

        public class WorkItemEventArgs : EventArgs
        {
            public WorkItem<TWorkItemState> WorkItem { get; set; }
        }
        
        private static readonly object WorkItemsLocker = new object();

        private int maxNumberOfConcurentWorkers = 10;
        /// <summary>
        /// Gets or sets the max number of concurent workers that can be working at the same time.
        /// </summary>
        /// <value>The max number of concurent workers.</value>
        public int MaxNumberOfConcurentWorkers
        {
            get { return maxNumberOfConcurentWorkers; }
            set { maxNumberOfConcurentWorkers = value; }
        }

        public Collection<WorkItem<TWorkItemState>> WorkItems = new Collection<WorkItem<TWorkItemState>>();
        
        private readonly ManualResetEvent allWorkItemsFinishedSignal = new ManualResetEvent(false);
        private readonly AutoResetEvent workItemFinishedSignal = new AutoResetEvent(false);

        /// <summary>
        /// Gets or sets a value indicating whether this instance is currently working.
        /// </summary>
        /// <value>
        /// 	<c>true</c> if this instance is working; otherwise, <c>false</c>.
        /// </value>
        public bool IsWorking { get; private set; }

        /// <summary>
        /// Adds the WorkItem to the queue
        /// </summary>
        /// <param name="workerMethod">The worker method.</param>
        /// <param name="state">The state.</param>
        /// <returns></returns>
        protected virtual WorkItem<TWorkItemState> EnqueueWork(Action<TWorkItemState> workerMethod, TWorkItemState state)
        {
            var item = new WorkItem<TWorkItemState>(workerMethod, state);
            lock (WorkItemsLocker)
            {
                this.WorkItems.Add(item);
            }
            return item;
        }

        /// <summary>
        /// Clears the WorkItems queue.
        /// </summary>
        protected void ClearWorkQueue()
        {
            if (this.IsWorking)
            {
                return;
            }

            lock (WorkItemsLocker)
            {
                this.WorkItems.Clear();
            }
        }

        /// <summary>
        /// Starts all the work asynchronously.
        /// </summary>
        public void DoAllWork()
        {
            if (this.IsWorking && !this.IsPaused)
            {
                return;
            }

            if (!this.IsPaused)
            {
                // its new start
                this.allWorkItemsFinishedSignal.Reset();
            }

            var worker = new Thread(this.DoAllWorkInternally);
            worker.Start();
        }
        
        private int TotalStarted
        {
            get
            {
                return (from item in this.WorkItems where item.IsStarted select item).Count();                
            }
        }

        private void DoAllWorkInternally()
        {
            if (this.IsWorking && !this.IsPaused)
            {
                return;
            }
            
            if (this.IsPaused)
            {
                // we are resuming
                this.IsPaused = false;
            }

            this.IsAborted = false;
            
            this.IsWorking = true;
            
            int totalSpawned = 0;

            var currentRunners = new Collection<WorkItemRunner<TWorkItemState>>();
            var currentResetEvents = new List<ManualResetEvent>();
            var currentWorkItems = new Collection<WorkItem<TWorkItemState>>();
            
            while ( ((totalSpawned < WorkItems.Count) || (currentResetEvents.Count > 0)) && (!IsAborted) && (!IsPaused))
            {
                totalSpawned = this.TotalStarted;
            
                int remaining = WorkItems.Count - totalSpawned;
                int workingNow = currentResetEvents.Count;

                int toDoNow = 0;
                if (totalSpawned < WorkItems.Count)
                {
                    toDoNow = remaining >= (MaxNumberOfConcurentWorkers - workingNow)
                                      ? (MaxNumberOfConcurentWorkers - workingNow)
                                      : remaining;
                }

                for (int i = 0; i < toDoNow; i++)
                {
                    var currentWorkItem = this.WorkItems[totalSpawned];
                    if (!currentWorkItem.IsCanceled)
                    {
                        var runner = new WorkItemRunner<TWorkItemState>(currentWorkItem);
                        runner.Run();
                        currentResetEvents.Add(runner.WorkDoneEvent);
                        currentRunners.Add(runner);
                        currentWorkItems.Add(currentWorkItem);
                        
                        this.OnWorkItemStarted(currentWorkItem);
                    }
                    else
                    {
                        this.OnWorkItemFinished(currentWorkItem);
                    }
                    
                    totalSpawned++;
                }

                var eventsToWaitFor = currentResetEvents.ToArray();

                if (eventsToWaitFor.Length > 0)
                {
                    // wait for any to finish
                    int finishedIndex = WaitHandle.WaitAny(eventsToWaitFor);
                    var runnerFinished = currentRunners[finishedIndex];
                    var workItemFinished = runnerFinished.WorkItem;

                    currentRunners.RemoveAt(finishedIndex);
                    currentResetEvents.RemoveAt(finishedIndex);

                    this.OnWorkItemFinished(workItemFinished);
                }
            }

            if (this.IsAborted)
            {
                foreach (var item in currentRunners)
                {
                    item.Abort();
                    this.OnWorkItemFinished(item.WorkItem);
                }
            }
            
            if (this.IsPaused)
            {
                var eventsToWaitFor = currentResetEvents.ToArray();

                if (eventsToWaitFor.Length > 0)
                {
                    // wait for any to finish
                    WaitHandle.WaitAll(eventsToWaitFor);

                    foreach (var runnerFinished in currentRunners)
                    {
                        var workItemFinished = runnerFinished.WorkItem;
                        this.OnWorkItemFinished(workItemFinished);                        
                    }
                }

                return;            
            }

            this.IsWorking = false;

            if (this.TotalStarted == this.WorkItems.Count)
            {
                this.OnAllWorkItemsFinished();
            }
        }

        public virtual void Abort()
        {
            this.IsAborted = true;
        }

        public bool IsAborted
        {
            get; set;
        }
        
        public void WaitForAllToFinish()
        {
            this.allWorkItemsFinishedSignal.WaitOne();
        }
        
        public void WaitForAnyToFinish()
        {
            this.workItemFinishedSignal.WaitOne();
        }

        /// <summary>
        /// Occurs when all download tasks are finished.
        /// </summary>
        protected event AllWorkItemsFinishedHandler AllWorkItemsFinished;


        private void OnAllWorkItemsFinished()
        {
            this.allWorkItemsFinishedSignal.Set();
            
            var handler = this.AllWorkItemsFinished;
            if (handler != null)
            {
                this.AllWorkItemsFinished();
            }
        }

        public bool IsPaused
        {
            get; private set;
        }
        
        
        public void Pause()
        {
            if (this.IsPaused)
            {
                return;
            }

            this.IsPaused = true;
        }
        
        public void Resume()
        {
            if (!this.IsPaused)
            {
                throw new Exception("Cannot resume if not paused");
            }

            this.DoAllWorkInternally();
        }

        private void OnWorkItemFinished(WorkItem<TWorkItemState> item)
        {
            var handler = this.WorkItemFinished;
            if (handler != null)
            {
                this.WorkItemFinished(this, new WorkItemEventArgs {WorkItem = item});
            }

            workItemFinishedSignal.Set();
        }

        private void OnWorkItemStarted(WorkItem<TWorkItemState> item)
        {
            var handler = this.WorkItemFinished;
            if (handler != null)
            {
                this.WorkItemStarted(this, new WorkItemEventArgs { WorkItem = item });
            }

            workItemFinishedSignal.Set();
        }        

        /// <summary>
        /// Occurs when single worker thread has finished.
        /// </summary>
        protected event WorkItemFinishedHandler WorkItemFinished;
        protected event WorkItemStartedHandler WorkItemStarted;
        
    }
}
